﻿using IMAPI2.Interop;
using IMAPI2.MediaItem;
using PSDLib;
using PSDLib.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Threading.Tasks;

namespace PSDBurnService
{
    public delegate void BurnStartEventHandler(RecorderDevice devcie);
    public delegate void BurnEndEventHandler(RecorderDevice devcie);
    public delegate void UpdateCapacityEventHandler(RecorderDevice device, Int64 totalDiscSize, Int64 totalMediaSize);
    public delegate void BurnProgressEventHandler(RecorderDevice device, dl_burn_dvd dvd, string info, int percent);
    public delegate void BurnSuccessEventhandler(RecorderDevice device, dl_burn_dvd dvd, string info);
    public delegate void BurnErrorEventHandler(RecorderDevice device, dl_burn_dvd dvd, string info);
    public delegate void DetectRecorderEventHandler(RecorderDevice device, string info);

    public class RecorderDevice
    {
        public event BurnStartEventHandler OnBurnStart;
        public event BurnEndEventHandler OnBurnEnd;
        public event UpdateCapacityEventHandler OnUpdateCapacity;
        public event BurnProgressEventHandler OnBurnProgress;
        public event BurnSuccessEventhandler OnBurnSuccess;
        public event BurnErrorEventHandler OnBurnError;
        public event DetectRecorderEventHandler OnDetectRecorder;

        private IMAPI_BURN_VERIFICATION_LEVEL _verificationLevel = IMAPI_BURN_VERIFICATION_LEVEL.IMAPI_BURN_VERIFICATION_NONE;

        Int64 _totalDiscSize = 0;
        private BurnServiceContext context;

        public string RecorderId { get; private set; }
        public string Name { get { return VolumePathName + "[" + ProductId + "]"; } }
        public string VolumePathName { get; private set; }
        public string ProductId { get; private set; }
        public IDiscRecorder2 Recorder { get; private set; }
        public RecorderState State { get; set; }
        public List<IMediaItem> MediaItemList { get; set; }

        private System.ComponentModel.BackgroundWorker backgroundBurnWorker;
        private bool _closeMedia;
        private bool _ejectMedia;
        private string _volumeName;
        private BurnState _burnData;
        public dl_burn_dvd CurrDVD { get; set; }

        public RecorderDevice(BurnServiceContext ctx, string uniqueRecorderId)
        {
            context = ctx;

            MediaItemList = new List<IMediaItem>();
            _burnData = new BurnState();

            if (Init(uniqueRecorderId))
            {
                State = RecorderState.Standby;
            }
            else
            {
                State = RecorderState.Disabled;
            }
        }

        /// <summary>
        /// 初始化刻录设备
        /// </summary>
        /// <param name="uniqueRecorderId"></param>
        /// <returns></returns>
        public bool Init(string uniqueRecorderId)
        {
            bool success = false;

            RecorderId = uniqueRecorderId;

            if (!string.IsNullOrEmpty(uniqueRecorderId))
            {
                Recorder = new MsftDiscRecorder2();
                Recorder.InitializeDiscRecorder(uniqueRecorderId);

                VolumePathName = Recorder.VolumePathNames[0].ToString();
                ProductId = Recorder.ProductId;

                this.backgroundBurnWorker = new System.ComponentModel.BackgroundWorker();
                this.backgroundBurnWorker.WorkerReportsProgress = true;
                this.backgroundBurnWorker.WorkerSupportsCancellation = true;
                this.backgroundBurnWorker.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundBurnWorker_DoWork);
                this.backgroundBurnWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundBurnWorker_RunWorkerCompleted);
                this.backgroundBurnWorker.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.backgroundBurnWorker_ProgressChanged);

                success = true;
            }


            return success;

        }

        /// <summary>
        /// 检测光驱中光盘信息，获得可用空间大小
        /// </summary>
        /// <returns></returns>
        public bool DetectRecorder()
        {
            bool success;

            MsftFileSystemImage fileSystemImage = null;
            MsftDiscFormat2Data discFormatData = null;

            try
            {
                //
                // Create and initialize the IDiscFormat2Data
                //
                discFormatData = new MsftDiscFormat2Data();
                if (!discFormatData.IsCurrentMediaSupported(this.Recorder))
                {
                    //labelMediaType.Text = "Media not supported!";
                    OnDetectRecorder?.Invoke(this, "不被支持的媒体");
                    _totalDiscSize = 0;
                    return false;
                }
                else
                {
                    //
                    // Get the media type in the recorder
                    //
                    discFormatData.Recorder = this.Recorder;
                    IMAPI_MEDIA_PHYSICAL_TYPE mediaType = discFormatData.CurrentPhysicalMediaType;
                    //labelMediaType.Text = GetMediaTypeString(mediaType);

                    //
                    // Create a file system and select the media type
                    //
                    fileSystemImage = new MsftFileSystemImage();
                    fileSystemImage.ChooseImageDefaultsForMediaType(mediaType);

                    //
                    // See if there are other recorded sessions on the disc
                    //
                    if (!discFormatData.MediaHeuristicallyBlank)
                    {
                        fileSystemImage.MultisessionInterfaces = discFormatData.MultisessionInterfaces;
                        fileSystemImage.ImportFileSystem();
                    }

                    Int64 freeMediaBlocks = fileSystemImage.FreeMediaBlocks;
                    _totalDiscSize = 2048 * freeMediaBlocks;

                    OnDetectRecorder?.Invoke(this, "媒体可用空间大小=" + PSDFunction.FormatByteSize(_totalDiscSize));
                }
            }
            catch (COMException exception)
            {
                //MessageBox.Show(exception.Message, "Detect Media Error",
                //    MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                if (discFormatData != null)
                {
                    Marshal.ReleaseComObject(discFormatData);
                }

                if (fileSystemImage != null)
                {
                    Marshal.ReleaseComObject(fileSystemImage);
                }
            }


            success = this.UpdateCapacity();

            return success;
        }

        /// <summary>
        /// 检测光盘是否有足够的剩余空间来刻录相关文件
        /// </summary>
        /// <returns></returns>
        public bool UpdateCapacity()
        {
            bool result = true;
            Int64 totalMediaSize = 0;
            //
            // Get the text for the Max Size
            //
            if (_totalDiscSize == 0)
            {
                result = false;
            }
            else
            {
                // Calculate the size of the filesc

                foreach (IMediaItem mediaItem in MediaItemList)
                {
                    totalMediaSize += mediaItem.SizeOnDisc;
                }

                if (totalMediaSize > _totalDiscSize)
                {
                    result = false;
                }

                if (totalMediaSize == 0)
                {
                    result = false;
                }
            }

            OnUpdateCapacity?.Invoke(this, _totalDiscSize, totalMediaSize);

            return result;
        }

        /// <summary>
        /// 添加文件集合
        /// </summary>
        /// <param name="fileList"></param>
        public void AddFiles(List<string> fileList)
        {
            MediaItemList.Clear();
            foreach (var file in fileList)
            {
                AddFile(file);
            }
        }

        /// <summary>
        /// 添加文件
        /// </summary>
        /// <param name="filePath"></param>
        public void AddFile(string filePath)
        {
            var fileItem = new FileItem(filePath);
            MediaItemList.Add(fileItem);
        }

        /// <summary>
        /// 开始刻录
        /// </summary>
        /// <param name="volumeName"></param>
        /// <param name="closeMedia"></param>
        /// <param name="ejectMedia"></param>
        public void StartBurn(string volumeName, bool closeMedia, bool ejectMedia)
        {
            _volumeName = volumeName;
            _closeMedia = closeMedia;
            _ejectMedia = ejectMedia;

            State = RecorderState.Burning;

            _burnData.uniqueRecorderId = this.RecorderId;



            backgroundBurnWorker.RunWorkerAsync(_burnData);
        }
        /// <summary>
        /// The thread that does the burning of the media
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundBurnWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            MsftDiscRecorder2 discRecorder = null;
            MsftDiscFormat2Data discFormatData = null;

            try
            {
                OnBurnStart?.Invoke(this);

                context.Logger.Info("开始刻录");

                OnBurnProgress?.Invoke(this, CurrDVD, "开始刻录", 0);
                //
                // Create and initialize the IDiscRecorder2 object
                //
                discRecorder = new MsftDiscRecorder2();
                var burnData = (BurnState)e.Argument;
                discRecorder.InitializeDiscRecorder(burnData.uniqueRecorderId);

                //
                // Create and initialize the IDiscFormat2Data
                //
                discFormatData = new MsftDiscFormat2Data
                {
                    Recorder = discRecorder,
                    ClientName = "PSD Burn",
                    ForceMediaToBeClosed = _closeMedia
                };

                //
                // Set the verification level
                //
                var burnVerification = (IBurnVerification)discFormatData;
                burnVerification.BurnVerificationLevel = _verificationLevel;

                //
                // Check if media is blank, (for RW media)
                //
                object[] multisessionInterfaces = null;
                if (!discFormatData.MediaHeuristicallyBlank)
                {
                    multisessionInterfaces = discFormatData.MultisessionInterfaces;
                }

                //
                // Create the file system
                //
                IStream fileSystem;
                if (!CreateMediaFileSystem(discRecorder, multisessionInterfaces, out fileSystem))
                {
                    e.Result = -1;
                    return;
                }

                //
                // add the Update event handler
                //
                discFormatData.Update += discFormatData_Update;

                //
                // Write the data here
                //
                try
                {
                    discFormatData.Write(fileSystem);
                    e.Result = 0;
                }
                catch (COMException ex)
                {
                    e.Result = ex.ErrorCode;
                    //MessageBox.Show(ex.Message, "IDiscFormat2Data.Write failed",
                    //    MessageBoxButtons.OK, MessageBoxIcon.Stop);


                }
                finally
                {
                    if (fileSystem != null)
                    {

                        Marshal.FinalReleaseComObject(fileSystem);
                    }
                }

                //
                // remove the Update event handler
                //
                discFormatData.Update -= discFormatData_Update;

                if (_ejectMedia)
                {
                    discRecorder.EjectMedia();
                }
            }
            catch (COMException exception)
            {
                //
                // If anything happens during the format, show the message
                //
                e.Result = exception.ErrorCode;

                //OnBurnError?.Invoke(this, CurrDVD, "Burn DoWork:" + exception.Message);

                //context.Logger.Error("刻录失败：" + exception.Message);
            }
            finally
            {
                if (discRecorder != null)
                {
                    Marshal.ReleaseComObject(discRecorder);
                }

                if (discFormatData != null)
                {
                    Marshal.ReleaseComObject(discFormatData);
                }
            }
        }

        /// <summary>
        /// 创建光盘文件系统
        /// </summary>
        /// <param name="discRecorder"></param>
        /// <param name="multisessionInterfaces"></param>
        /// <param name="dataStream"></param>
        /// <returns></returns>
        private bool CreateMediaFileSystem(IDiscRecorder2 discRecorder, object[] multisessionInterfaces, out IStream dataStream)
        {
            MsftFileSystemImage fileSystemImage = null;
            try
            {
                fileSystemImage = new MsftFileSystemImage();
                fileSystemImage.ChooseImageDefaults(discRecorder);
                fileSystemImage.FileSystemsToCreate =
                    FsiFileSystems.FsiFileSystemJoliet | FsiFileSystems.FsiFileSystemISO9660;
                fileSystemImage.VolumeName = _volumeName;

                fileSystemImage.Update += fileSystemImage_Update;

                //
                // If multisessions, then import previous sessions
                //
                if (multisessionInterfaces != null)
                {
                    fileSystemImage.MultisessionInterfaces = multisessionInterfaces;
                    fileSystemImage.ImportFileSystem();
                }

                //
                // Get the image root
                //
                IFsiDirectoryItem rootItem = fileSystemImage.Root;
                //
                // Add Files and Directories to File System Image
                //
                foreach (IMediaItem mediaItem in this.MediaItemList)
                {
                    //
                    // Check if we've cancelled
                    //
                    if (backgroundBurnWorker.CancellationPending)
                    {
                        break;
                    }
                    //
                    // Add to File System
                    //
                    mediaItem.AddToFileSystem(rootItem);
                }

                fileSystemImage.Update -= fileSystemImage_Update;
                //
                // did we cancel?
                //
                if (backgroundBurnWorker.CancellationPending)
                {
                    dataStream = null;
                    return false;
                }

                dataStream = fileSystemImage.CreateResultImage().ImageStream;
            }
            catch (COMException exception)
            {
                //MessageBox.Show(this, exception.Message, "Create File System Error",
                //    MessageBoxButtons.OK, MessageBoxIcon.Error);
                OnBurnError?.Invoke(this, CurrDVD, "Create File System Error:" + exception.Message);
                dataStream = null;
                return false;
            }
            finally
            {
                if (fileSystemImage != null)
                {
                    Marshal.ReleaseComObject(fileSystemImage);
                }
            }

            return true;
        }

        /// <summary>
        /// 光盘文件系统数据更新事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="currentFile"></param>
        /// <param name="copiedSectors"></param>
        /// <param name="totalSectors"></param>
        void fileSystemImage_Update([In, MarshalAs(UnmanagedType.IDispatch)] object sender,
            [In, MarshalAs(UnmanagedType.BStr)] string currentFile, [In] int copiedSectors, [In] int totalSectors)
        {
            var percentProgress = 0;
            if (copiedSectors > 0 && totalSectors > 0)
            {
                percentProgress = (copiedSectors * 100) / totalSectors;
            }

            if (!string.IsNullOrEmpty(currentFile))
            {
                var fileInfo = new FileInfo(currentFile);
                _burnData.statusMessage = "Adding \"" + fileInfo.Name + "\" to image...";
                System.Diagnostics.Debug.WriteLine(_burnData.statusMessage);
                //
                // report back to the ui
                //
                _burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_FILE_SYSTEM;
                backgroundBurnWorker.ReportProgress(percentProgress, _burnData);
            }
        }

        /// <summary>
        /// 更新刻录情况，并通知刻录进度事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="progress"></param>
        void discFormatData_Update([In, MarshalAs(UnmanagedType.IDispatch)] object sender, [In, MarshalAs(UnmanagedType.IDispatch)] object progress)
        {
            //
            // Check if we've cancelled
            //
            if (backgroundBurnWorker.CancellationPending)
            {
                var format2Data = (IDiscFormat2Data)sender;
                format2Data.CancelWrite();
                return;
            }

            var eventArgs = (IDiscFormat2DataEventArgs)progress;

            _burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING;

            // IDiscFormat2DataEventArgs Interface
            _burnData.elapsedTime = eventArgs.ElapsedTime;
            _burnData.remainingTime = eventArgs.RemainingTime;
            _burnData.totalTime = eventArgs.TotalTime;

            // IWriteEngine2EventArgs Interface
            _burnData.currentAction = eventArgs.CurrentAction;
            _burnData.startLba = eventArgs.StartLba;
            _burnData.sectorCount = eventArgs.SectorCount;
            _burnData.lastReadLba = eventArgs.LastReadLba;
            _burnData.lastWrittenLba = eventArgs.LastWrittenLba;
            _burnData.totalSystemBuffer = eventArgs.TotalSystemBuffer;
            _burnData.usedSystemBuffer = eventArgs.UsedSystemBuffer;
            _burnData.freeSystemBuffer = eventArgs.FreeSystemBuffer;

            //context.Logger.Log("更新刻录数据 burnData = " + (_burnData.lastWrittenLba - _burnData.startLba));
            //System.Diagnostics.Debug.WriteLine("burnData = " + (_burnData.lastWrittenLba - _burnData.startLba));
            //
            // Report back to the UI
            //
            backgroundBurnWorker.ReportProgress(0, _burnData);
        }

        /// <summary>
        /// 刻录完成事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundBurnWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            State = RecorderState.Standby;

            if ((int)e.Result == 0)
            {
                OnBurnSuccess?.Invoke(this, CurrDVD, "刻录成功");
            }
            else
            {
                OnBurnError?.Invoke(this, CurrDVD, (e.Error == null ? "" : e.Error.Message));
            }

            OnBurnEnd?.Invoke(this);
        }

        /// <summary>
        /// 刻录进度事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundBurnWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            //int percent = e.ProgressPercentage;
            var burnData = (BurnState)e.UserState;
            string status = "";
            int percent = 0;

            if (burnData.task == BURN_MEDIA_TASK.BURN_MEDIA_TASK_FILE_SYSTEM)
            {
                status = burnData.statusMessage;
            }
            else if (burnData.task == BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING)
            {
                switch (burnData.currentAction)
                {
                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VALIDATING_MEDIA:
                        status = "Validating current media...";
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FORMATTING_MEDIA:
                        status = "Formatting media...";
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_INITIALIZING_HARDWARE:
                        status = "Initializing hardware...";
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_CALIBRATING_POWER:
                        status = "Optimizing laser intensity...";
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_WRITING_DATA:
                        long writtenSectors = burnData.lastWrittenLba - burnData.startLba;

                        if (writtenSectors > 0 && burnData.sectorCount > 0)
                        {
                            percent = (int)((100 * writtenSectors) / burnData.sectorCount);
                            status = string.Format("Progress: {0}%", percent);
                            //statusProgressBar.Value = percent;
                        }
                        else
                        {
                            status = "Progress 0%";
                            percent = 0;
                            //statusProgressBar.Value = 0;
                        }
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FINALIZATION:
                        status = "Finalizing writing...";
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_COMPLETED:
                        status = "Completed!";
                        break;

                    case IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VERIFYING:
                        status = "Verifying";
                        break;
                }
            }
            //System.Diagnostics.Debug.WriteLine(status + " " + percent);
            OnBurnProgress?.Invoke(this, CurrDVD, status, percent);
        }
    }

    public enum RecorderState : int
    {
        Disabled = -1,
        Standby = 0,
        Burning = 1,
    }
}
